// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
var R = typeof Reflect === "object" ? Reflect : null;
var ReflectApply =
        R && typeof R.apply === "function"
        ? R.apply
        : function ReflectApply(target, receiver, args) {
        return Function.prototype.apply.call(target, receiver, args);
    };

var ReflectOwnKeys;
if (R && typeof R.ownKeys === "function") {
    ReflectOwnKeys = R.ownKeys;
} else if (Object.getOwnPropertySymbols) {
    ReflectOwnKeys = function ReflectOwnKeys(target) {
        return Object.getOwnPropertyNames(target).concat(
            Object.getOwnPropertySymbols(target)
        );
    };
} else {
    ReflectOwnKeys = function ReflectOwnKeys(target) {
        return Object.getOwnPropertyNames(target);
    };
}

function ProcessEmitWarning(warning) {
    if (console && console.warn) console.warn(warning);
}

var NumberIsNaN =
    Number.isNaN ||
    function NumberIsNaN(value) {
        return value !== value;
    };

function EventEmitter() {
    EventEmitter.init.call(this);
}

var EventEmitter_1 = EventEmitter;
var events = EventEmitter;
var once_1 = once;

// Backwards-compat with node 0.10.x
EventEmitter.EventEmitter = EventEmitter;

EventEmitter.prototype._events = undefined;
EventEmitter.prototype._eventsCount = 0;
EventEmitter.prototype._maxListeners = undefined;

// By default EventEmitters will print a warning if more than 10 listeners are
// added to it. This is a useful default which helps finding memory leaks.
var defaultMaxListeners = 10;

function checkListener(listener) {
    if (typeof listener !== "function") {
        throw new TypeError(
            'The "listener" argument must be of type Function. Received type ' +
            typeof listener
        );
    }
}

Object.defineProperty(EventEmitter, "defaultMaxListeners", {
    enumerable: true,
    get: function () {
        return defaultMaxListeners;
    },
    set: function (arg) {
        if (typeof arg !== "number" || arg < 0 || NumberIsNaN(arg)) {
            throw new RangeError(
                'The value of "defaultMaxListeners" is out of range. It must be a non-negative number. Received ' +
                arg +
                "."
            );
        }
        defaultMaxListeners = arg;
    },
});

EventEmitter.init = function () {
    if (
        this._events === undefined ||
        this._events === Object.getPrototypeOf(this)._events
    ) {
        this._events = Object.create(null);
        this._eventsCount = 0;
    }

    this._maxListeners = this._maxListeners || undefined;
};

// Obviously not all Emitters should be limited to 10. This function allows
// that to be increased. Set to zero for unlimited.
EventEmitter.prototype.setMaxListeners = function setMaxListeners(n) {
    if (typeof n !== "number" || n < 0 || NumberIsNaN(n)) {
        throw new RangeError(
            'The value of "n" is out of range. It must be a non-negative number. Received ' +
            n +
            "."
        );
    }
    this._maxListeners = n;
    return this;
};

function _getMaxListeners(that) {
    if (that._maxListeners === undefined) return EventEmitter.defaultMaxListeners;
    return that._maxListeners;
}

EventEmitter.prototype.getMaxListeners = function getMaxListeners() {
    return _getMaxListeners(this);
};

EventEmitter.prototype.emit = function emit(type) {
    var args = [];
    for (var i = 1; i < arguments.length; i++) args.push(arguments[i]);
    var doError = type === "error";

    var events = this._events;
    if (events !== undefined) doError = doError && events.error === undefined;
    else if (!doError) return false;

    // If there is no 'error' event listener then throw.
    if (doError) {
        var er;
        if (args.length > 0) er = args[0];
        if (er instanceof Error) {
            // Note: The comments on the `throw` lines are intentional, they show
            // up in Node's output if this results in an unhandled exception.
            throw er; // Unhandled 'error' event
        }
        // At least give some kind of context to the user
        var err = new Error(
            "Unhandled error." + (er ? " (" + er.message + ")" : "")
        );
        err.context = er;
        throw err; // Unhandled 'error' event
    }

    var handler = events[type];

    if (handler === undefined) return false;

    if (typeof handler === "function") {
        ReflectApply(handler, this, args);
    } else {
        var len = handler.length;
        var listeners = arrayClone(handler, len);
        for (var i = 0; i < len; ++i) ReflectApply(listeners[i], this, args);
    }

    return true;
};

function _addListener(target, type, listener, prepend) {
    var m;
    var events;
    var existing;

    checkListener(listener);

    events = target._events;
    if (events === undefined) {
        events = target._events = Object.create(null);
        target._eventsCount = 0;
    } else {
        // To avoid recursion in the case that type === "newListener"! Before
        // adding it to the listeners, first emit "newListener".
        if (events.newListener !== undefined) {
            target.emit(
                "newListener",
                type,
                listener.listener ? listener.listener : listener
            );

            // Re-assign `events` because a newListener handler could have caused the
            // this._events to be assigned to a new object
            events = target._events;
        }
        existing = events[type];
    }

    if (existing === undefined) {
        // Optimize the case of one listener. Don't need the extra array object.
        existing = events[type] = listener;
        ++target._eventsCount;
    } else {
        if (typeof existing === "function") {
            // Adding the second element, need to change to array.
            existing = events[type] = prepend
                ? [listener, existing]
                : [existing, listener];
            // If we've already got an array, just append.
        } else if (prepend) {
            existing.unshift(listener);
        } else {
            existing.push(listener);
        }

        // Check for listener leak
        m = _getMaxListeners(target);
        if (m > 0 && existing.length > m && !existing.warned) {
            existing.warned = true;
            // No error code for this since it is a Warning
            // eslint-disable-next-line no-restricted-syntax
            var w = new Error(
                "Possible EventEmitter memory leak detected. " +
                existing.length +
                " " +
                String(type) +
                " listeners " +
                "added. Use emitter.setMaxListeners() to " +
                "increase limit"
            );
            w.name = "MaxListenersExceededWarning";
            w.emitter = target;
            w.type = type;
            w.count = existing.length;
            ProcessEmitWarning(w);
        }
    }

    return target;
}

EventEmitter.prototype.addListener = function addListener(type, listener) {
    return _addListener(this, type, listener, false);
};

EventEmitter.prototype.on = EventEmitter.prototype.addListener;

EventEmitter.prototype.prependListener = function prependListener(
        type,
        listener
) {
    return _addListener(this, type, listener, true);
};

function onceWrapper() {
    if (!this.fired) {
        this.target.removeListener(this.type, this.wrapFn);
        this.fired = true;
        if (arguments.length === 0) return this.listener.call(this.target);
        return this.listener.apply(this.target, arguments);
    }
}

function _onceWrap(target, type, listener) {
    var state = {
        fired: false,
        wrapFn: undefined,
        target: target,
        type: type,
        listener: listener,
    };
    var wrapped = onceWrapper.bind(state);
    wrapped.listener = listener;
    state.wrapFn = wrapped;
    return wrapped;
}

EventEmitter.prototype.once = function once(type, listener) {
    checkListener(listener);
    this.on(type, _onceWrap(this, type, listener));
    return this;
};

EventEmitter.prototype.prependOnceListener = function prependOnceListener(
        type,
        listener
) {
    checkListener(listener);
    this.prependListener(type, _onceWrap(this, type, listener));
    return this;
};

// Emits a 'removeListener' event if and only if the listener was removed.
EventEmitter.prototype.removeListener = function removeListener(
        type,
        listener
) {
    var list, events, position, i, originalListener;

    checkListener(listener);

    events = this._events;
    if (events === undefined) return this;

    list = events[type];
    if (list === undefined) return this;

    if (list === listener || list.listener === listener) {
        if (--this._eventsCount === 0) this._events = Object.create(null);
        else {
            delete events[type];
            if (events.removeListener)
                this.emit("removeListener", type, list.listener || listener);
        }
    } else if (typeof list !== "function") {
        position = -1;

        for (i = list.length - 1; i >= 0; i--) {
            if (list[i] === listener || list[i].listener === listener) {
                originalListener = list[i].listener;
                position = i;
                break;
            }
        }

        if (position < 0) return this;

        if (position === 0) list.shift();
        else {
            spliceOne(list, position);
        }

        if (list.length === 1) events[type] = list[0];

        if (events.removeListener !== undefined)
            this.emit("removeListener", type, originalListener || listener);
    }

    return this;
};

EventEmitter.prototype.off = EventEmitter.prototype.removeListener;

EventEmitter.prototype.removeAllListeners = function removeAllListeners(type) {
    var listeners, events, i;

    events = this._events;
    if (events === undefined) return this;

    // not listening for removeListener, no need to emit
    if (events.removeListener === undefined) {
        if (arguments.length === 0) {
            this._events = Object.create(null);
            this._eventsCount = 0;
        } else if (events[type] !== undefined) {
            if (--this._eventsCount === 0) this._events = Object.create(null);
            else delete events[type];
        }
        return this;
    }

    // emit removeListener for all listeners on all events
    if (arguments.length === 0) {
        var keys = Object.keys(events);
        var key;
        for (i = 0; i < keys.length; ++i) {
            key = keys[i];
            if (key === "removeListener") continue;
            this.removeAllListeners(key);
        }
        this.removeAllListeners("removeListener");
        this._events = Object.create(null);
        this._eventsCount = 0;
        return this;
    }

    listeners = events[type];

    if (typeof listeners === "function") {
        this.removeListener(type, listeners);
    } else if (listeners !== undefined) {
        // LIFO order
        for (i = listeners.length - 1; i >= 0; i--) {
            this.removeListener(type, listeners[i]);
        }
    }

    return this;
};

function _listeners(target, type, unwrap) {
    var events = target._events;

    if (events === undefined) return [];

    var evlistener = events[type];
    if (evlistener === undefined) return [];

    if (typeof evlistener === "function")
        return unwrap ? [evlistener.listener || evlistener] : [evlistener];

    return unwrap
        ? unwrapListeners(evlistener)
        : arrayClone(evlistener, evlistener.length);
}

EventEmitter.prototype.listeners = function listeners(type) {
    return _listeners(this, type, true);
};

EventEmitter.prototype.rawListeners = function rawListeners(type) {
    return _listeners(this, type, false);
};

EventEmitter.listenerCount = function (emitter, type) {
    if (typeof emitter.listenerCount === "function") {
        return emitter.listenerCount(type);
    } else {
        return listenerCount.call(emitter, type);
    }
};

EventEmitter.prototype.listenerCount = listenerCount;

function listenerCount(type) {
    var events = this._events;

    if (events !== undefined) {
        var evlistener = events[type];

        if (typeof evlistener === "function") {
            return 1;
        } else if (evlistener !== undefined) {
            return evlistener.length;
        }
    }

    return 0;
}

EventEmitter.prototype.eventNames = function eventNames() {
    return this._eventsCount > 0 ? ReflectOwnKeys(this._events) : [];
};

function arrayClone(arr, n) {
    var copy = new Array(n);
    for (var i = 0; i < n; ++i) copy[i] = arr[i];
    return copy;
}

function spliceOne(list, index) {
    for (; index + 1 < list.length; index++) list[index] = list[index + 1];
    list.pop();
}

function unwrapListeners(arr) {
    var ret = new Array(arr.length);
    for (var i = 0; i < ret.length; ++i) {
        ret[i] = arr[i].listener || arr[i];
    }
    return ret;
}

function once(emitter, name) {
    return new Promise(function (resolve, reject) {

        function errorListener(err) {
            emitter.removeListener(name, resolver);
            reject(err);
        }

        function resolver() {
            if (typeof emitter.removeListener === "function") {
                emitter.removeListener("error", errorListener);
            }
            resolve([].slice.call(arguments));
        }

        eventTargetAgnosticAddListener(emitter, name, resolver, { once: true });
        if (name !== "error") {
            addErrorHandlerIfEventEmitter(emitter, errorListener, { once: true });
        }
    });
}

function addErrorHandlerIfEventEmitter(emitter, handler, flags) {
    if (typeof emitter.on === "function") {
        eventTargetAgnosticAddListener(emitter, "error", handler, flags);
    }
}

function eventTargetAgnosticAddListener(emitter, name, listener, flags) {
    if (typeof emitter.on === "function") {
        if (flags.once) {
            emitter.once(name, listener);
        } else {
            emitter.on(name, listener);
        }
    } else if (typeof emitter.addEventListener === "function") {
        // EventTarget does not have `error` event semantics like Node
        // EventEmitters, we do not listen for `error` events here.
        emitter.addEventListener(name, function wrapListener(arg) {
            // IE does not have builtin `{ once: true }` support so we
            // have to do it manually.
            if (flags.once) {
                emitter.removeEventListener(name, wrapListener);
            }
            listener(arg);
        });
    } else {
        throw new TypeError(
            'The "emitter" argument must be of type EventEmitter. Received type ' +
            typeof emitter
        );
    }
}

events.EventEmitter = EventEmitter_1;
events.once = once_1;

export { EventEmitter_1 as EventEmitter, events as default, once_1 as once };
